js进阶- 面向对象编程

(一) 面向过程编程和面向对象编程(了解)

**面向过程编程(Procedure Oriented Programming)**就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。

**面向对象编程(Object-Oriented Programming)**是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。

具体的实现我们看一下最经典的“把大象放冰箱”这个问题

(1) 面向过程的解决方法

以过程(步骤)为核心

第一步: 开门(冰箱); 第二步: 装进(冰箱,大象); 第三步: 关门(冰箱)。

// 开门的方法, todo是等待去实现的意思
function openDoor() {
    // todo
}
// 装进去的方法
function putInto() {
    // todo
}
// 关门的方法
function closeDoor() {
    // todo
}

// step1
openDoor();
// step2
putInfo();
// step3
closeDoor();

(2) 面向对象的解决方法

以对象为核心

冰箱.开门() 冰箱.装进(大象) 冰箱.关门()

var iceBox = {
  openDoor() {
    // todo
  },
  putInto() {
    // todo
  },
  closeDoor() {
    // todo
  },
};
iceBox.openDoor();
iceBox.putInto();
iceBox.closeDoor();

又比如办一个驾照:

要获得一个驾照, 你必须一个个部分去跑, 跑完所有流程, 都通过了就拿到了驾照

你也可以交钱一个相关公司, 他们派一个人帮你, 那个人熟悉没一个流程, 所以面向对象还有个好处是, 你根本不需要功能具体是怎么实现, 直接调用对象的方法就好了, 比如axios.js

(二) 面向对象编程知识图谱(了解)

我们主要掌握以下几点:

  1. 如何创建对象
    • 工厂模式
    • 构造函数模式
    • 原型模式
  2. 对象对象三大特性
    • 封装:对象是将数据与功能组合到一起,即就是将属性与方法封装起来
    • 继承:所谓继承就是自己没有, 别人有,拿过来为自己所用, 并成为自己的东西
    • 多态:多态其实就是把做的内容和谁去做分开。

img

(三) js创建对象

(1) 使用工厂模式创建对象

用来创建功能类似的对象

function createPerson(name,age,job) {
  // 创建一个空对象
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.say = function() {
    console.log(o.name);
  } 
  return o;
}

var p1 = createPerson('张三',20,'测试工程师');
var p2 = createPerson('李四',20,'前端工程师');
var p3 = createPerson('小芬',19,'UI工程师');
console.log(p1,p2,p3);

(2) 使用构造函数模式创建对象

function Person(name, age, job) { 
  this.name = name;
  this.age = age;
  this.job = job; 
  this.sayName = function() {
    console.log(this.name);
  }
}
 
var p1 = new Person("张三", 29, "测试工程师");
var p2 = new Person("李四", 28, "前端工程师");
p1.sayName();
p2.sayName();

问题1: 构造函数的this指向了谁

  • 使用new 的时候, this指向了所创建的实例
  • 不适用new 的时候, this指向了window

问题2: js中的new ()到底做了些什么

使用new来创建对象时, 系统"偷偷的"做了以下动作:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) ;
  3. 执行构造函数中的代码(为这个新对象添加属性和方法) ;
  4. 返回新对象。

(3) 使用原型模式创建对象

function Person() {
   
}
// prototype指向了Person原型(原型对象)
// 给prototype添加属性
Person.prototype.name = '中国人';
Person.prototype.type = '人类';
// 给prototype添加方法
Person.prototype.say = function() {
  console.log(this.name);
} 

var p = new Person();
console.log(p.type); 
p.say();

(四) 原型(原型对象)和原型链

(1) 原型继承

// 指针, 引用, 内存地址, 属性 是一码事

原型对象的四个特点:

  1. 每一个构造函数都有一个prototype属性, 指向了函数的原型对象

    比喻: 原型对象同时也是构造函数实例的隐形的"父亲"

  2. 实例继承原型对象所有的属性和方法

  3. 实例有一个内部指针 __proto__ 指向了原型对象

    Person.prototype === p1.__proto__;

  4. 原型对象有一个constructor属性指向了构造函数(了解)

    Person.prototype.constructor === Person

function Person(name,age) {
  this.name = name;
  this.age = age;
}

Person.prototype.father = '老王';
Person.prototype.age = 30;
Person.prototype.getFather = function() {
  console.log(this.father);
}

// p1,p2是Person"生产出来的",可以把Person看成是母亲,p1,p2则为儿子
var p1 = new Person('狗蛋',1);
var p2 = new Person('二胖',2);
console.log(p1);
console.log(p2);

图示:

image-20220211154102349

(2) 原型链

  1. 每一个构造函数都有一个prototype属性指向了原型对象, 它的实例都有一个内部指针__proto 指向了原型对象
  2. 如果原型对象同时是另外一个类型(构造函数)的实例, 那么原型对象内部也会有一个内部指针指向了另外一个原型对象
  3. 假如另外一个原型对象又是另外一个类型(构造函数)的实例, 上述关系依然成立, 如此层层递进就形成了实例与原型对象链接, 称为原型链
  4. 原型链的尽头是Object的原型对象, 印证了 '一切皆对象' 这句话
<script> 
   function Father() {
       this.fatherName = '阿三';
       this.fatherAge = 60;
   }  
   Father.prototype.type = '人类';

   function Son() {
       this.sonName = '小三';
       this.sonAge = 25;
   }

   // 创建father的实例
   var father = new Father(); 
   // 实现原型继承
   Son.prototype = father;

   var son = new Son();
   console.log(son);  

   function GrandSon() {

   }
   GrandSon.prototype = son; 
   var grandSon = new GrandSon();
   console.log(grandSon) 

	// 一切皆对象
	grandSon.__proto__.__proto__.__proto__.__proto__ === Object.prototype;  // true
	var date = new Date();
	date.__proto__.__proto__ === Object.prototype; // true
</script>

image-20220211163426590

(五) js多态(了解)

多态是同一个行为具有多个不同表现形式或形态的能力。

多态的思想实际上是把“想做什么”和“谁去做“分开,多态的好处是什么呢?

多态的最根本好处在于,你不必再向对象询问“你是什么类型”而后根据得到的答案调用对象的某个行为,你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。

    // 开始唱歌
function startSing(animal) {
    animal.sing(); 
}

function Duck() { }
Duck.prototype.sing = function() {
    console.log('嘎嘎嘎');
}

function Dog() { }
Dog.prototype.sing = function() {
    console.log('汪汪汪');
}

var duck = new Duck();
var dog = new Dog();

startSing(duck);
startSing(dog); 

(六) js继承

(1) 原型继承

// 人的构造函数
function Person() {}
Person.prototype.type = "人类";
Person.prototype.say = function () {
  console.log("这是个人类");
};

// 继承: 让男人的原型对象等于Person的一个实例
Man.prototype =  new Person();

function Man(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
}
var man = new Man("张三", 20, "男");
console.log(man);
// type和say都是继承而来
console.log('type',man.type);
man.say();

原型继承的缺点:

  • 父类的引用类型属性会被所有子类实例共享,任何一个子类实例修改了父类的引用类型属性,其他子类实例都会受到影响
  • 创建子类实例的时候,不能向父类传参

(2) Es6的继承方式

class

  1. Es6新数据类型class, 用来创建对象
  2. constructor 构造器, 当调用new Person(), 构造器里的方法就会运行
class Person{
   // 构造器(其实就是es5里的构造函数)
   constructor(nation) {
    this.nation = nation;
   }

   say() {
       console.log(`这是一个${this.nation}`);
   }

   sing() {
       console.log('人类会唱歌');
   }
   
}

var p = new Person('中国');
console.log(p);
p.say();
p.sing();

实现继承

// es6的类, 用来创建对象
class Person{
   // 构造器(其实就是es5里的构造函数)
   constructor(nation) {
    this.nation = nation;
    this.type = '人类';
   }

   say() {
       console.log(`这是一个${this.nation}`);
   }

   sing() {
       console.log('人类会唱歌');
   }
   
}


// 定义一个女人的类
class Woman extends Person{
    constructor(nation) {
        // 调用父类构造器
        super(nation);
        this.sex = '女人';
    }
}

var w = new Woman('中国');
console.log('国籍',w.nation);
console.log('类别',w.type);
w.sing();

(3) 其它继承方式

1. 借用构造函数继承

function Father(name) {
  this.name = name;
  this.say = function () {
    console.log("hello");
  };
}

function Child(name) {
  this.name = name;
  // 调用Father方法
  Father.call(this, name);
}

var p1 = new Child("张三");
p1.say();

优点:

  1. 避免了引用类型属性被所有实例共享
  2. 可以向父类传参

缺点:

  1. 方法必须定义在构造函数中
  2. 每创建一个实例都会创建一遍方法

2. 组合继承(原型继承和借用构造函数继承的组合)

function Father(name, age) {
	this.name = name;
	this.age = age;
	console.log(this);
}
Father.prototype.say = function() {
	console.log('hello');
}

function Child(name,age) {
	Father.call(this,name,age);
}
Child.prototype = new Father();

var child = new Child('Tom', 22);
console.log(child);  

常用的继承方式唯一的缺点是,父类的构造函数会被调用两次

3. 寄生式继承

function createObj(o) {
	var clone = object.create(o);
	clone.sayName = function () {
		console.log('hello');
	}
	return clone;
}

var father = {
  name: '人',
  nation: '中国'
}

var son = createObj(father);
// son继承了father,拥有father的属性,同时还拥有sayName的方法
console.log(son);

就是创建一个封装的函数来增强原有对象的属性,跟借用构造函数一样,每个实例都会创建一遍方法

4. 寄生组合式继承

es5最完美的继承方式

// 原型+借用构造函数+寄生
function Person() {
	console.log(22);
	this.class = '人类';
}

// 原型继承模式
Person.prototype.say = function() {
	console.log(this.name);
}

/**
 * 寄生 Man.prototype.__proto__ === Person.prototype;
 * Man的父亲的父亲 === Person的父亲
 */
 Man.prototype = Object.create(Person.prototype);
 Man.prototype.constructor = Man;


function Man(name, age) {
	this.name = name;
	this.age = age;
	// 借用构造函数模式
	Person.call(this);
}

var man = new Man('张三丰', 100);
console.log(man);

这是es5最好的继承方式,集合了所有继承方式的优点于一身。

  • 原型继承可以访问父类原型上定义的方法
  • 寄生继承,父类构造器不需要被调用两次
  • 借用构造函数,可以把父类构造器中的属性放入子类的实例

总结: 三种简单的继承方式

  1. 原型式继承
  2. 借用构造函数继承
  3. 寄生式继承

两种复杂的继承方式

  1. 组合式继承: 1+2的组合
  2. 寄生组合式继承: 1+2+3的组合

TIP

设计模式

(七) 什么是设计模式

假设有一个空房间,我们要日复一日地往里 面放一些东西。最简单的办法当然是把这些东西 直接扔进去,但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东 西的位置也不容易。所以在房间里做一些柜子也 许是个更好的选择,虽然柜子会增加我们的成 本,但它可以在维护阶段为我们带来好处。使用 这些柜子存放东西的规则,或许就是一种模式。

(1) 工厂模式

  1. 工厂模式是一种用来创建对象的设计模式。
  2. 我们不暴露对象创建的逻辑,而是将逻辑封装在一个函数内,那么这个函数可以成为工厂。
  3. 作用(应用场景): 用来创建相似对象时执行一些重复操作。
function bookFatory(name, year, vs) {
  var book = new Object();
  book.name = name;
  book.year = year;
  book.vs = vs;
  book.price = "暂无标价";
  if (name === "JS高级编程") {
    book.price = "79";
  }
  if (name === "css世界") {
    book.price = "69";
  }
  if (name === "VUE权威指南") {
    book.price = "89";
  }
  return book;
}
var book1 = bookFatory("JS高级编程", "2013", "第三版");
var book2 = bookFatory("ES6入门教程", "2017", "第六版");
var book3 = bookFatory("css世界", "2015", "第一版");
console.log(book1);
console.log(book2);
console.log(book3);

(2) 单例模式

单例就是保证一个类只有一个实例,实现的方法一般是先判断实例存在与否,如果存在直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象好处是可以减少不必要的内存开销

应用场景:

  • 数据库连接对象
  • 网站的登录弹窗,无论在别的地方点击多少次,都只会弹出一个登录弹窗
  • 全局数据存储对象(vuex)
// 模拟数据库连接对象
<script>
  // 声明构造函数Db,用来创建数据库连接对象
  var obj;
  function Db() { 
    if (obj) {
      return obj;
    } else {
      this.find = function () {
        console.log("查找数据");
      };
      this.delete = function () {
        console.log("删除数据");
      };
      this.update = function () {
        console.log("更新数据");
      };
      obj = this;
    }
  }

  var db1 = new Db();
  var db2 = new Db();
  console.log(db1 === db2);
</script>

(3) 发布-订阅模式

发布---订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。

现实生活中的发布-订阅模式;

比如小红最近在淘宝网上看上一双鞋子,但是呢 联系到卖家后,才发现这双鞋卖光了,但是小红对这双鞋又非常喜欢,所以呢联系卖家,问卖家什么时候有货,卖家告诉她,要等一个星期后才有货,卖家告诉小红,要是你喜欢的话,你可以收藏我们的店铺,等有货的时候再通知你,所以小红收藏了此店铺,但与此同时,小明,小花等也喜欢这双鞋,也收藏了该店铺;等来货的时候就依次会通知他们;

在上面的故事中,可以看出是一个典型的发布订阅模式,卖家是属于发布者,小红,小明等属于订阅者,订阅该店铺,卖家作为发布者,当鞋子到了的时候,会依次通知小明,小红等,依次使用旺旺等工具给他们发布消息;
发布订阅模式的优点:

1.支持简单的广播通信,当对象状态发生改变时,会自动通知已经订阅过的对象。 2.比如上面的列子,小明,小红不需要天天逛淘宝网看鞋子到了没有,在合适的时间点,发布者(卖家)来货了的时候,会通知该订阅者(小红,小明等人)。

对于第一点,我们日常工作中也经常使用到,比如我们的ajax请求,请求有成功(success)和失败(error)的回调函数,我们可以订阅ajax的success和error事件。我们并不关心对象在异步运行的状态,我们只关心success的时候或者error的时候我们要做点我们自己的事情就可以了~
例子1: js的事件就是发布-订阅模式
<script>
  var button = document.querySelector('.btn');
  // 绑定点击事件 => 订阅报纸
  button.addEventListener('click',reader1,false);
  function reader1() {
    console.log('读者1');
  }
  button.addEventListener('click',reader2, false);
  function reader2() {
    console.log('读者2')
  } 

  setTimeout(function() {
    // 发布报纸
    button.click();
  },3000);
</script>
用js实现发布-订阅
class Event {
  constructor () {}
  // 首先定义一个事件容器,用来装事件数组(因为订阅者可以是多个)
  handlers = {}

  // 事件添加方法,参数有事件名和事件方法
  addEventListener (type, handler) {
    // 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
    if (!(type in this.handlers)) {
      this.handlers[type] = []
    }
    // 将事件存入
    this.handlers[type].push(handler)
  }

  // 触发事件两个参数(事件名,参数)
  dispatchEvent (type, ...params) {
    // 若没有注册该事件则抛出错误
    if (!(type in this.handlers)) {
      return new Error('未注册该事件')
    }
    // 便利触发
    this.handlers[type].forEach(handler => {
      handler(...params)
    })
  }

  // 事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和发布)
  removeEventListener (type, handler) {
      // 无效事件抛出
      if (!(type in this.handlers)) {
        return new Error('无效事件')
      }
      if (!handler) {
        // 直接移除事件
        delete this.handlers[type]
      } else {
        const idx = this.handlers[type].findIndex(ele => ele === handler)
        // 抛出异常事件
        if (idx === undefined) {
          return new Error('无该绑定事件')
        }
        // 移除事件
        this.handlers[type].splice(idx, 1)
        if (this.handlers[type].length === 0) {
          delete this.handlers[type]
        }
      }
    }
}


var event = new Event() // 创建event实例
// 定义一个自定义事件:"load"
function load (params) {
  console.log('load', params)
}
event.addEventListener('load', load)
// 再定义一个load事件
function load2 (params) {
  console.log('load2', params)
}
event.addEventListener('load', load2)
// 触发该事件
event.dispatchEvent('load', 'load事件触发')
// 移除load2事件
event.removeEventListener('load', load2)
// 移除所有load事件
event.removeEventListener('load')

作业

  1. 说出以下代码运行结果, 并解释为什么

    var param = 1;
    function main() { 
      console.log('1',param);
      var param = 2;
      console.log('2',this.param);
      this.param = 3;
    }
    
    main(); 
    var m = new main(); 
    
  2. 说出以下代码运行结果, 并解释为什么

    知识点:

    1. 给构造函数添加方法(静态方法)
    2. 函数和变量声明提前
    3. 原型对象

    坑:

    • 函数和变量声明提前(函数声明在最前面)
    • 函数是一等公民
    function Foo() {
      getName = function() {
        console.log('1');
      };
      return this;
    } 
    Foo.getName = function() {
      console.log('2');
    } 
    Foo.prototype.getName = function() {
      console.log('3');
    } 
    var getName = function() {
      console.log('4');
    } 
    function getName() {
      console.log('5');
    }
    
    Foo.getName();  
    getName();   
    Foo().getName();
    getName();   
    new Foo.getName();  
    new Foo().getName();  
    

    image-20220214093531519